Vue+Koa2移动电商项目学习笔记(二)

在项目中安装 Koa2

在项目中安装 Koa 和 MongoDB

  1. 首先在项目根目录下建立文件夹 service,并进入文件夹

  2. 使用 npm init -y 生成并初始化 package.js 文件

  3. 在命令行使用 npm 来安装 koa

    npm install –save koa

编写一个 Hello World 测试安装是否成功

1
2
3
4
5
6
7
8
9
10
11
//	service 目录下新建 index.js
const Koa = require( 'koa' )
const app = new Koa()

app.use(async(ctx)=>{
ctx.body = '<h1>Hello World</h1>'
})

app.listen(3000,()=>{
console.log('[Server] starting at port 3000')
})

编写好后,使用 node index.js 来启动服务,然后在浏览器中输入 http://localhost:3000,如果正常显示 hello world,说明 koa2 已经安装成功啦。

安装 MongoDB 数据库

安装步骤

  1. 在官网下载 MongoDB
  2. 下载之后进行安装,选择默认安装
  3. 安装时如果有安全软件报拦截一律允许就好啦,否则胡安装失败
  4. 安装完成后,需要配置 “环境变量”, 目的是在命令行中直接使用,而不用驶入很长的路径

运行 MongoDB 服务端

安装好 MongoDB 数据库之后,需要启用服务端才能使用。启用服务的命令是: Mongod

  1. 打开命令行: win + R ===> cmd
  2. 执行 mongod : 在命令中直接输入 mongod,会发现服务并没有启动,报了一个 exception,服务停止
  3. 新建文件夹:出现上面的错误,是因为还没有建立 MongoDB 需要的文件夹,一般是安装盘的根目录,建立 data/db,这两个文件夹
  4. 运行 mongod:这时候服务就可以开启了,链接默认端口是 27017

下载 Robo3

由于我们是做项目,所以图形界面比较直观,我们上边没有安装图形界面,这里使用 Robo3 来弥补一下

下载地址:Robo3

然后就是下一步下一步安装

Koa 用 Mongoose 连接数据库 (1)

Mongoose 概念

Mongoose 是一个开源的封装好的实现 Node 和 MongoDB 数据通讯的数据建模库

Mongoose 安装

npm install mongoose –save // 使用 npm 进行安装

连接数据库

在项目 service 文件夹下新建一个 database 文件夹,用来存放和数据库操作相关的文件。 在 database 文件夹下, 新建一个 init.js 文件,用来做数据库的连接和一些初始化的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//	/service/database/init.js
const mongoose = require('mongoose')
const db = "mongodb://localhost/simle-db"

mongoose.Promise = global.Promise

exports.connect = ()=>{
//连接数据库
mongoose.connect(db)

//增加数据库连接的事件监听
mongoose.connection.on('disconnected',()=>{
//进行重连
mongoose.connect(db)
})

//数据库出现错误的时候
mongoose.connection.on('error',err=>{
console.log(err)
mongoose.connect(db)
})

//链接打开的时候
mongoose.connection.once('open',()=>{
console.log('MongoDB Connected successfully!')
})

}

然后在 /service/index.js 里加入立即执行函数,在使用之前要使用 require 进行引入 connect

1
2
3
4
5
6
7
//引入connect
const {connect} = require('./database/init.js')

//立即执行函数
;(async () =>{
await connect()
})()

然后在终端中运行 node index.js 命令,就可以看到数据库已经连接成功了

Koa 用 Mongoose 连接数据库 (2)

前面已经做了基本的数据库连接,并且已经连接成功。但是如果数据没有开启,或者网络出现问题,我们并没有做这些意外处理,在写程序时,当主要功能完成时,我们要作意外处理和逻辑处理,让程序增加健壮性

增加 Promise 支持

在做 init.js 文件时,必须确保先连接数据库后,在做其他事情,所以需要在所有代码的外层增加一个 Promise

1
2
3
return new Promise ((resolve,reject)=>{
// 把所有连接放在这里
})

连接失败自动重连

一般数据库连接失败,我们会重新连接,但这个重连也是需要有一个次数的,比如连接 3 次失败,我们在服务端抛出异常

首先声明一个最大连接数 maxConnectTimes.

let maxConnectTimes = 0

当连接断开时,进行重连的代码如下

1
2
3
4
5
6
7
8
9
10
11
mongoose.connection.on('disconnected',()=>{
console.log('***********数据库断开***********')
if(maxConnectTimes<3){
maxConnectTimes++
mongoose.connect(db)
}else{
reject()
throw new Error('数据库出现问题,程序无法搞定,请人为修理......')
}

})

当连接断开时,需要把连接次数 +1,并重连数据库。当重连次数超过 3 次后,我们抛出异常,并用 reject() 通知 Promise

同样当连接出错时,我们也要进行重新连接操作

1
2
3
4
5
6
7
8
9
10
11
mongoose.connection.on('error',err=>{
console.log('***********数据库错误***********')
if(maxConnectTimes<3){
maxConnectTimes++
mongoose.connect(db)
}else{
reject(err)
throw new Error('数据库出现问题,程序无法搞定,请人为修理......')
}

})

全部文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const mongoose = require('mongoose')
const db = "mongodb://localhost/smile-db"

exports.connect = ()=>{
//连接数据库
mongoose.connect(db)

let maxConnectTimes = 0

return new Promise((resolve,reject)=>{
//把所有连接放到这里

//增加数据库监听事件
mongoose.connection.on('disconnected',()=>{
console.log('***********数据库断开***********')
if(maxConnectTimes<3){
maxConnectTimes++
mongoose.connect(db)
}else{
reject()
throw new Error('数据库出现问题,程序无法搞定,请人为修理......')
}

})

mongoose.connection.on('error',err=>{
console.log('***********数据库错误***********')
if(maxConnectTimes<3){
maxConnectTimes++
mongoose.connect(db)
}else{
reject(err)
throw new Error('数据库出现问题,程序无法搞定,请人为修理......')
}

})
//链接打开的时
mongoose.connection.once('open',()=>{
console.log('MongoDB connected successfully')
resolve()
})

})

}

Mongoose 的 Schema 建模

数据库已经可以连接成功了,现在我们来了解一下如何建模,也就是定义 Schema,相当于 MongoDB 数据库的一个映射。 Schema 是一种以文件形式存储的数据库模型骨架,无法直接通往数据库端,也就是说它不具备对数据库的操作能力。 Schema 是以 key-value 形式 json 格式的数据。

Schema 中的数据类型

  • String:字符串类型
  • Number:数字类型
  • Date:日期类型
  • Boolean:布尔类型
  • Buffer:NodeJS buffer 类型
  • ObjectID:主键,一种特殊而非常重要的类型
  • Mixed:混合类型
  • Array:集合类型

Mongoose 中的三个概念

  • schema:用来定义表的模板,实现和 MongoDB 数据库的映射。用来实现每个字段的类型,长度,映射的字段,不具备表的操作能力。
  • model: 具备某张表操作能力的一个集合,是 mongoose 的核心能力。
  • entity: 类似记录,由 Model 创建的实体,也具有数据库的操作能力。

初学定义一个用户 Schema

我们先以用户表为例,定义一个基本数据模型,( 当然并不完善,后面会逐步完善这个模型,并加入一些安全的机制进去 )

在 /service/database/ 文件夹下新建一个 schema 文件夹, 然后新建一个 User.js 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mongoose = require('mongoose')    //引入Mongoose
const Schema = mongoose.Schema //声明Schema
let ObjectId = Schema.Types.ObjectId //声明Object类型

//创建我们的用户Schema
const userSchema = new Schema({
UserId:ObjectId,
userName:{unique:true,type:String},
password:String,
createAt:{type:Date,default:Date.now()},
lastLoginAt:{type:Date,default:Date.now()}

})

//发布模型
mongoose.model('User',userSchema)

载入 Schema 和插入查出数据

Schema 建立好以后,需要载入这些数据库,最好的方法就是在后台服务已启动的时候就把载入做好,所以在 service/init.js 里做这件事情,然后在 index.js 里直接执行

载入所有 Schema

在 service/init.js 引入一个 glob 和一个 resolve

首先安装 glob

npm install glob –save

const glob = require(‘glob’)

const { resolve } = require(‘path’)

  • glob:node 的 glob 模块允许使用 * 等符号,来写一个 glob 规则,像在 shell 里一样,获取匹配对应规则文件
  • resolve:将一系列路径或路径段解析为绝对路径
1
2
3
4
//	了解两个引入的模块用法后,就可以一次性引入所有的 Schema 文件了。
exports.initSchemas = () => {
glob.sync( resolve(__dirname, './schema/', '**/*.js')).forEach(require)
}

插入一条数据

在操作数据库之前先引入我们的 Mongoose 和 刚写好的 initSchemas

1
2
const mongoose = require('mongoose')
const {connect , initSchemas} = require('./database/init.js')

引入好后,直接在 service/index.js 的立即执行函数里插入一条 User 数据

1
2
3
4
5
6
7
8
9
10
;(async () =>{
await connect()
initSchemas()
const User = mongoose.model('User')
let oneUser = new User({userName:'jspang',password:'123456'})
oneUser.save().then(()=>{
console.log('插入成功')
})

})()

读出已经插入进去的数据

1
2
3
4
5
let  users = await  User.findOne({}).exec()
console.log('------------------')
console.log(users)
console.log('------------------')
// 这里只读出了一条,我们也可以试着读出多条

完整的 index.js 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const Koa = require('koa')
const app = new Koa()
const mongoose = require('mongoose')
const {connect , initSchemas} = require('./database/init.js')

//立即执行函数
;(async () =>{
await connect()
initSchemas()
const User = mongoose.model('User')
let oneUser = new User({userName:'jspang13',password:'123456'})

oneUser.save().then(()=>{
console.log('插入成功')

})
let users = await User.findOne({}).exec()

console.log('------------------')
console.log(users)
console.log('------------------')
})()


app.use(async(ctx)=>{
ctx.body = '<h1>hello Koa2</h1>'
})

app.listen(3000,()=>{
console.log('[Server] starting at port 3000')
})

打造安全的用户密码加密机制

加密处理

密码加密有很多种算法,比如 MD5 加密或者 hash256 加密算法;可以通过 哈希加密算法 这个网站直观地了解一下加密算法

加盐处理

有时候用户的密码设置的太过简单,很容易受到暴力破解或者用彩虹表破解。这时候就要使用加盐技术了,其实就是在原来的密码里,加入一些其他的字符串,并且我们可以自己设置加入字符串的强度。

把加盐的数据库密码进行 hash 处理后,在存入数据库就比较安全了

bcrypt 的使用

bcrypt 是一种跨平台的文件加密工具。有它加密的文件可在所有支持的操作系统和处理器上进行转译。它的口令必须是 8 至 56 个字符,并将在内部被转化为 448 位的密钥

  • 使用 npm 进行安装 // 安装在 service 文件夹里面

    npm install –save –registry=https://registry.npm.taobao.org

    ​ 因为 bcrypt 里边的二进制包的下载可能被墙掉了,可以尝试使用淘宝源来进行安装

  • 引入 bcrypt

    const bcrypt = require( ‘bcrype’ ) // /service/database/schema/User.js 文件

  • 用 pre 每次进行保存时都进行加盐加密的操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*	 /service/database/schema/User.js 文件	*/

    //每次存储数据时都要执行
    userSchema.pre('save', function(next){
    // let user = this
    // console.log(this)
    bcrypt.genSalt( SALT_WORK_FACTOR,(err,salt)=>{
    if(err) return next(err)
    bcrypt.hash(this.password,salt, (err,hash)=>{
    if(err) return next(err)
    this.password = hash
    next()
    })

    })
    })

编写注册页面前端视图

注册页面的 vue 模板编写

  1. 新建页面

    首先,来新建一个 vue 的模板文件: src/components/pages/Register.vue

  2. 编写 vue 的路由配置文件 router/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import Vue from 'vue'
    import Router from 'vue-router'
    import ShoppingMall from '@/components/pages/ShoppingMall'
    import Register from '@/components/pages/Register'

    Vue.use(Router)

    export default new Router({
    routes: [
    {path: '/',name: 'ShoppingMall',component: ShoppingMall},
    {path: '/register',name: 'Register',component: Register},
    ]
    })
  3. 引入 Vant 的两个插件 Field 和 NavBar

    1
    2
    3
    //	main.js
    import { Button, Row, Col, Search, Swipe, SwipeItem, Lazyload, List, Field, NavBar } from 'vant'
    Vue.use(Button).use(Row).use(Col).use(Search).use(Swipe).use(SwipeItem).use(Lazyload).use(List).use(Field).use(NavBar)
  1. 编写模板文件,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    <template>
    <div>
    <van-nav-bar
    title="用户注册"
    left-text="返回"
    left-arrow
    @click-left="goBack"
    />

    <div class="register-panel">
    <van-field
    v-model="username"
    label="用户名"
    icon="clear"
    placeholder="请输入用户名"
    required
    @click-icon="username = ''"
    />

    <van-field
    v-model="password"
    type="password"
    label="密码"
    placeholder="请输入密码"
    required
    />
    <div class="register-button">
    <van-button type="primary" size="large">马上注册</van-button>
    </div>
    </div>

    </div>
    </template>

    <script>
    export default {
    data() {
    return {
    username: '',
    password: '',
    }
    },
    methods: {
    goBack() {
    this.$router.go(-1)
    }
    },
    }
    </script>

    <style scoped>
    .register-panel{
    width:96%;
    border-radius: 5px;
    margin:20px auto;
    padding-bottom:50px;
    }
    .register-button{
    padding-top:10px;
    }
    </style>

Koa2 的用户操作的路由模块化

把所有的路由都写在 service/index.js 里是不正确的选择,这回导致我们的 index.js 页面越来越臃肿,最后变得没办法维护。我们需要把 Koa 程序模块化,也叫做路由模块化

安装 Koa-router

进入到 service 文件夹下,打开命令行,使用 npm 安装:

npm install koa-router –save // 这里下载的版本是 7.4.0

新建一个 User.js 文件

新建一个 appApi 文件夹,然后进入文件夹,新建 user.js 文件。有关 User.js 的操作,以后都会放到这个文件下,就是要编写的供前台使用的接口程序

编写 user.js 文件:

1
2
3
4
5
6
7
8
9
const Router = require ( 'koa-router' )
let router = new Router()
router.get('/',async(ctx)=>{
ctx.body = "这是用户操作首页"
})
router.get('/register',async(ctx)=>{
ctx.body = "用户注册接口"
})
module.exports = router;

让路由模块化

  1. 首先在 index.js 的文件顶部,引入 koa-router

    const Router = require(‘koa-router’)

  2. 引入 user.js 模块

    let user = require(‘./appApi/user.js’)

  3. 装载所有子路由

    let router = new Router();

    router.use(‘/user’,user.routes())

  4. 加载路由中间件

    app.use(router.routes())

    app.use(router.allowedMethods())

    做完这四步,我们就可以在浏览器中试一下我们的模块化路由是否起作用了,在浏览器中输入 localhost:3000/user,已经可以出现我们设定好的页面了 ( 需要首先使用 node index.js 启动服务 )

总结: 通过这种简单的模块化路由机制,我们就实现了文件的分离,当然这并不是最完美的方案,如果对要求比较高的朋友,可以看一下 egg.js 的路由写法或者直接使用 egg.js 来进行开发。当然现在这种做法完全可以应付小型项目的开发

打通注册用户的前后端通讯

这一步的主要作用是 使用 API 接口的形式可以在前后端互相通讯和传递数据

安装 koa-bodyparser 中间件

首先要接到前端发过来的请求,这时候需要安装 koa-bodyparser 中间件,进入到 server 目录下,使用 npm 来进行安装

npm install –save koa-bodyparser // 这里安装的是 4.2.1 版本

安装好之后, 在 service/index.js 文件中注册和引入中间件

const bodyParser = require( ‘koa-bodyparser’ )

app.use(bodyParser());

前台的 axios 请求数据

  1. 在 register.vue 头部引入 axios

    import axios from ‘axios’

  2. 修改 serviceAPI.config.js 接口配置文件

    需要对接口配置文件做一些设置。加入我们的注册接口地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const BASEURL = "https://www.easy-mock.com/mock/5ae2eeb23fbbf24d8cd7f0b6/SmileVue/"
    const LOCALURL = "http://localhost:3000/"
    const URL = {
    getShoppingMallInfo:BASEURL+'index',
    getGoodsInfo:BASEURL+'getGoodsInfo',
    registerUser:LOCALURL+'user/register', //用户注册接口
    }

    module.exports = URL

    // 这里主要加入了 LOCALURL 常量的声明,用于存储本地请求路径,和在 URL 里增加了 registerUser 接口的地址配置
  3. 引入接口配置文件

    import url from ‘@/serviceAPI.config.js’

  4. 编写 axios 用户注册方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //	将下面的代码写在 src/components/pages/Register.vue 文件下的 methods 属性里面

    //*********axios注册用户方法********
    axiosRegisterUser(){
    axios({
    url: url.registerUser,
    method: 'post',
    data:{
    username:this.username,
    password:this.password
    }
    })
    .then(response => {
    console.log(response)
    })
    .catch((error) => {
    console.log(error)
    })
    }
  5. 给注册按钮绑定 axiosRegisterUser 方法

    马上注册

让 koa2 支持跨域请求

  • 安装 koa2-cors 中间件

    在 koa2 里解决跨域的中间件叫 koa2-cors,先进入到 service 文件夹,使用 npm 来进行安装

npm install –save koa2-cors

  • 在 service/index.js 文件中引入和注册 (使用) 中间件:

    const cors = require( ‘koa2-cors’ )

    app.use(cors())

编写 koa2 接收前台数据的方法

现在前台和后台数据互通的基本环节已经做好了,我们再写一个后台的数据接口方法,就可以实现接收数据,并回传数据了

进入 service/appApi/user.js 文件,修改 register 路由接口下的代码,记得把 get 方法换成 post 方法。

1
2
3
4
router.post('/register',async(ctx)=>{
console.log(ctx.request.body)
ctx.body = ctx.request.body
})

至此我们已经完成了前后台数据的互通,现在我们可以打开服务接口,开启浏览器测试一下

用户注册数据库操作

Koa2 的 User.js 接口的完善

  • 首先在 service/appApi/user.js 下引入 mongose,这样就可以操作我们的 Schema 了。

    const mongoose = require(‘mongoose’)

  • 编写 register 接口的程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    router.post('/register',async(ctx)=>{

    //取得Model
    const User = mongoose.model('User')
    //把从前端接收的POST数据封装成一个新的user对象
    let newUser = new User(ctx.request.body)
    //用mongoose的save方法直接存储,然后判断是否成功,返回相应的结果
    await newUser.save().then(()=>{
    //成功返回code=200,并返回成功信息
    ctx.body={
    code:200,
    message:'注册成功'
    }
    }).catch(error=>{
    //失败返回code=500,并返回错误信息
    ctx.body={
    code:500,
    message:error
    }
    })

    })

前端 vue 的业务处理

前面我们只是使用 axios 发送了一个请求,对返回的结果只是简单的 console.log 了一下,现在我们把这些代码进行补全,根据 koa 端返回的 code 进行不同的提示。

我们可以使用 Vant 的轻提示插件 Toast,先引入 Tosat 组件,直接在 Register.vue 下引入就可以了

import { Toast } from ‘vant’

具体业务逻辑代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
axiosRegisterUser(){
axios({
url: url.registerUser,
method: 'post',
data:{
userName:this.username,
password:this.password
}
})
.then(response => {
console.log(response)
//如果返回code为200,代表注册成功,我们给用户作Toast提示
if(response.data.code == 200){
Toast.success('注册成功')
}else{
console.log(response.data.message)
Toast.fail('注册失败')
}
console.log(response.data.code)
})
.catch((error) => {
Toast.fail('注册失败')
})

}

然后可以测试一下,看看是否可以插入数据库,并给用户一个有好的提示

注册的防重复提交

1. 在按钮上绑定 loading 属性

首先打开 src/components/pages/Register.vue 文件,找到注册按钮,然后在按钮上绑定 loadding 属性

马上注册

然后在线面的 JavaScript 部分的 data 中声明 openLoading 属性

1
2
3
4
5
6
7
data() {
return {
username: '',
password: '',
openLoading: false, //是否开启用户的Loading
}
},

2. 改写注册方法

防重复提交要在进入注册业务逻辑的时候就开始实行,也就是点击注册按钮这一步时。所以我们现在要改造已经写好的 axiosRegisterUser() 方法,加入一些防重复提交的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
axios({
url: url.registerUser,
method: 'post',
data:{
userName:this.username,
password:this.password
}
})
.then(response => {
console.log(response)
//如果返回code为200,代表注册成功,我们给用户作Toast提示
if(response.data.code == 200){
Toast.success('注册成功')
this.$router.push('/')
}else{
console.log(response.data.message)
Toast.fail('注册失败')
this.openLoading=false
}

})
.catch((error) => {
Toast.fail('注册失败')
this.openLoading=false
})

在一开始进入注册方法的时候,做的第一件事就是把注册按钮变成 loading 状态。然后在注册失败的时候取消 loading 状态,注册成功就跳转到个人中心页面 (这部分还没做). 这样在前台就防止了重复提交

解决一个小问题

在程序中定义的是 user 而真实数据库中变成了 users,我们可以在 Schema 里配置一下解决这个问题

打开 service/database/schema/User.js 修改 new 之后的代码。 加入 { collection: ‘user’ }

1
2
3
4
5
6
7
8
9
const userSchema = new Schema({
UserId:ObjectId,
userName:{unique:true,type:String},
password:String,
createAt:{type:Date,default:Date.now()},
lastLoginAt:{type:Date,default:Date.now()},
},{
collection:'user'
})

注册时的前端验证

1. 首先为 Field 绑定 error-message 属性

vant 框架提供的 field 属性提供了错误提示的机制,就是 error-message 属性。先在 script 的 data 里注册两个属性, usernameErrorMsg 和 passwordErrorMsg。 当值不符时,作用户提示使用。

1
2
3
4
5
6
7
8
9
10
11
12
//	在 template 里面的相应位置添加上 :error-message = "usernameErrorMsg" 和 :error-message = "passwordErrorMsg"

data(){
return {
username: '',
passwoed: '',
openLoading: false, // 是否开启用户的 Loading
usernameErrorMsg: '', // 当用户名出错的时候
passwordErrorMsg: '' // 当密码出错的时候
}
}
// 开始这两个值都为空,不给用户任何操作提示,只有按下注册按钮时才进行检测提示

2. 编写验证方法 checkForm

在 methods 里增加一个 checkForm() 方法,用来验证表单信息,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
checkForm(){
let isOk = true
if(this.username.length < 5){ // 验证用户名不能少于 5 位
this.usernameErrorMsg = "用户名不能小于 5 位"
isOk = false
} else {
this.usernameErrorMsg = ""
}
if(this.password.length < 6){ // 验证密码不能少于 6 位
this.passwordErrorMsg = "密码不能少于 6 位"
isOK = false
} else {
this.passwordErrorMsg = ""
}
return isOk
}

3. 重新编写注册方法

这时我们把一个注册分为了两个业务逻辑,第一步是检验表单数据,第二步向端口发送数据等待结果。如果直接在以前的 axiosRegisterUser() 方法上改造显得很不优雅,代码会很长,维护起来会增加额外的成本。新建一个 registerAction() 方法。

1
2
3
registerAction(){
this.checkForm() && this.axiosRegisterUser()
},

4. 重新绑定注册按钮事件

原来的按钮事件直接调用了 axiosRegisterUser() 方法,这时候我们要更换为新的 registerAction() 方法。

马上注册

Vue 的登陆界面制作和路由配置

1. 新建 Login.vue 文件

在 src/components/pages 文件夹下新建 Login.vue 文件。 因为登录页和注册页样式基本相同,所以可以直接复制注册页代码 然后进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<template>
<div>
<van-nav-bar
title="用户登录"
left-text="返回"
left-arrow
@click-left="goBack"
/>

<div class="register-panel">
<van-field
v-model="username"
label="用户名"
icon="clear"
placeholder="请输入用户名"
required
@click-icon="username = ''"
:error-message="usernameErrorMsg"
/>

<van-field
v-model="password"
type="password"
label="密码"
placeholder="请输入密码"
:error-message="passwordErrorMsg"
required
/>
<div class="register-button">
<van-button type="primary" @click="LoginAction" :loading="openLoading" size="large">登录</van-button>
</div>
</div>

</div>
</template>

<script>
import axios from 'axios'
import url from '@/serviceAPI.config.js'
import { Toast } from 'vant'
export default {
data() {
return {
username: '',
password: '',
openLoading: false, //是否开启用户的Loading
usernameErrorMsg:'', //当用户名出现错误的时候
passwordErrorMsg:'', //当密码出现错误的时候
}
},
methods: {
goBack() {
this.$router.go(-1)
},

//*****注册用户的实行方法*****
LoginAction(){

this.checkForm() && this.axiosLoginUser()
},

//*******axios注册用户方法*******
axioLoginUser(){
//先把按钮进行loading状态,防止重复提交
this.openLoading = true

axios({
url: url.registerUser,
method: 'post',
data:{
userName:this.username,
password:this.password
}
})
.then(response => {


})
.catch((error) => {

})

},
//**** 客户端验证
checkForm(){
let isOk= true
if(this.username.length<5){
this.usernameErrorMsg="用户名不能小于5位"
isOk= false
}else{
this.usernameErrorMsg=''
}
if(this.password.length<6){
this.passwordErrorMsg="密码不能少于6位"
isOk= false
}else{
this.passwordErrorMsg=''
}
return isOk
}


},
}
</script>

<style scoped>
.register-panel{
width:96%;
border-radius: 5px;
margin:20px auto;
padding-bottom:50px;
}
.register-button{
padding-top:10px;
}
</style

2. 配置路由,让页面可以正常访问

打开 /src/router/index.js 页面,配置路由,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import Router from 'vue-router'
import ShoppingMall from '@/components/pages/ShoppingMall'
import Register from '@/components/pages/Register'
import Login from '@/components/pages/Login'

Vue.use(Router)

export default new Router({
routes: [
{path: '/',name: 'ShoppingMall',component: ShoppingMall},
{path: '/register',name: 'Register',component: Register},
{path: '/login',name: 'Login',component: Login},
]
})

配置好之后启动服务,打开 http://localhost:8080/#/login 进行测试。

登陆的服务端业务逻辑代码

登陆业务的简单业务逻辑就是得到前端发来的用户名和密码,然后跟数据库进行比对,如果正确就显示登陆成功,失败就显示登陆失败

Schema中的对比实例方法

需要在 Schema 中制作一个比对的实例方法,这个方法就是比对我们加盐加密后的密码的。在 service/database/schema/User.js 中增加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
userSchema.methods = {
// 密码比对的方法
comparePassword:(_password,password) => {
return new Promise((resolve,reject) =>{
bcrypt.compare(_password,password,(err,isMatch) => {
if (!err) resolve(isMatch)
else reject(err)
})
})
}
}

// 这段代码声明了一个实例方法,方法叫做 comparePassword, 然后传递两个参数,一个是客户端密码,一个是数据库取出来的密码。用 bcrypt 提供的 compare 方法就可以比对,最后包装成 Promise 返回就可以了。

编写登陆的 API 接口

进入 service/appApi/user.js,增加一个 login 路由, 并在路由内写入业务逻辑代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*登录的实践 */
router.post('/login',async(ctx)=>{
//得到前端传递过来的数据
let loginUser = ctx.request.body
console.log(loginUser)
let userName = loginUser.userName
let password = loginUser.password
//引入User的model
const User = mongoose.model('User')
//查找用户名是否存在,如果存在开始比对密码
await User.findOne({userName:userName}).exec().then(async(result)=>{
console.log(result)
if(result){
//console.log(User)
//当用户名存在时,开始比对密码
let newUser = new User() //因为是实例方法,所以要new出对象,才能调用
await newUser.comparePassword(password,result.password)
.then( (isMatch)=>{
//返回比对结果
ctx.body={ code:200, message:isMatch}
})
.catch(error=>{
//出现异常,返回异常
console.log(error)
ctx.body={ code:500, message:error}
})
}else{
ctx.body={ code:200, message:'用户名不存在'}
}

}).catch(error=>{
console.log(error)
ctx.body={ code:500, message:error }
})
})

前后端结合调试

  • 在 src/serviceAPI.config.js 下加入接口代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const BASEURL = "https://www.easy-mock.com/mock/5ae2eeb23fbbf24d8cd7f0b6/SmileVue/"
    const LOCALURL = "http://localhost:3000/"
    const URL = {
    getShoppingMallInfo:BASEURL+'index',
    getGoodsInfo:BASEURL+'getGoodsInfo',
    registerUser:LOCALURL+'user/register', //用户注册接口
    login:LOCALURL+'user/login', //用户注册接口
    }

    module.exports = URL
  • 修改 src/components/pages/Login.vue 把 axios 的 Url 改成我们的 login 接口地址。

然后打开浏览器进行调试,就会得到成功与不成功。

登录的前端交互效果制作和登录状态存储

前端交互效果制作

目前为止从 Koa2 服务端是可以取到登录结果的,但是前端只是简单的打印了出来,并没有做任何交互。我们要在返回登录成功时,给用户一个 Toast 提示,并跳转到首页 ( 个人中心页面,暂未做 ),当返回登录失败的时候,要提示用户登录失败,并把登录按钮重新启用,可以再次登录

在 /src/components/pages/Login.vue 的 axiosLoginUser 方法里修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
axiosLoginUser(){
// 先把注册按钮进行 loading 状态,防止重复提交
this.openLoading = true;

axios({
url: url.login,
method: 'post',
data: {
userName: this.username,
password: this.password
}
})
.then(response => {
console.log(response)
if (response.data.code == 200 && response.data.message){
Toast.success('登录成功!')
this.$router.push('/')
} else {
Toast.fail('登录失败!')
this.openLoading = false
}
})
.catch((error) => {
console.log(error);
Toast.fail('登录失败!')
this.openLoading = false
})
},

保存用户登录状态

移动端的应用有一个特殊的地方,就是当用户登录一次后,下次就不用登录了。这时候登录的信息是存储到了本地的 LocalStorage 里了。这个操作要等取到正确的登陆状态以后再执行,也就是要在 axios 返回了登录成功结果以后执行。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
new Promise((resolve,reject) => {
localStorage.userInfo = {userName: this.username}
setTimeout(()=>{
resolve()
},500)
}).then(()=>{
Toast.success('登录成功!')
this.$router.push('/')
}).catch(err=>{
Toast.fail('登录状态保存失败')
console.log(err)
})

我们保存了用户登录状态以后,就有了一个是否登录的依据,然后我们就不会重复登录了,我们在已进入登录页面的 created 生命周期里,就判断是否已经登录。

1
2
3
4
5
6
created(){
if(localStorage.userInfo){
Toast.success('您已经登录了')
this.$router.push('/')
}
},

这时候如果已经登录后,再去登录页他会直接跳转到首页,并且提示你已经登录过了。

这步做完后 Login.vue 页面 script 部分完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import axios from 'axios'
import url from '@/serviceAPI.config.js'
import { Toast } from 'vant'
export default {
data() {
return {
username: '',
password: '',
openLoading: false, //是否开启用户的Loading
usernameErrorMsg:'', //当用户名出现错误的时候
passwordErrorMsg:'', //当密码出现错误的时候
}
},
created(){
if(localStorage.userInfo){
Toast.success('您已经登录过了!')
this.$router.push('/');
}
},
methods: {
goBack() {
this.$router.go(-1)
},

//*****注册用户的实行方法*****
LoginAction(){

this.checkForm() && this.axiosLoginUser()
},

//*******axios注册用户方法*******
axiosLoginUser(){
//先把按钮进行loading状态,防止重复提交
this.openLoading = true

axios({
url: url.login,
method: 'post',
data:{
userName:this.username,
password:this.password
}
}).then(response => {
//如果返回code为200,代表注册成功,我们给用户作Toast提示
if(response.data.code == 200 && response.data.message){

new Promise((resolve,reject) => {
localStorage.userInfo = {userName: this.username}
setTimeout(()=>{resolve()},500)
}).then(()=>{
Toast.success('登录成功')
this.$router.push('/')
}).catch(err=>{
Toast.fail('登录状态保存失败')
console.log(err)
})

}else{
console.log(response.data.message)
Toast.fail('登陆失败')
this.openLoading = false
}
}).catch((error) => {
Toast.fail('登陆失败')
this.openLoading = false
})

},
//**** 客户端验证
checkForm(){
let isOk= true
if(this.username.length<5){
this.usernameErrorMsg="用户名不能小于5位"
isOk= false
}else{
this.usernameErrorMsg=''
}
if(this.password.length<6){
this.passwordErrorMsg="密码不能少于6位"
isOk= false
}else{
this.passwordErrorMsg=''
}
return isOk
}
},
}

商品详细数据的提纯操作

现在开始准备页面的商品数据和类别数据,目的是制作列表页和详情页。我们需要在 5W 多条商品详情的 JSON 数据中把有用的数据筛选出来,这种筛选叫做数据的提纯。

用 fs 读入数据

在 service 文件夹下,新建一个 fsJson.js 的文件使用 node 的 fs 模块,可以轻松把文件读取到程序中,然后进行遍历,把有用的数据提取出来,写入到一个新的数组中。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs')
fs.readFile('.goods.json','utf8',function(err,data){
let newData = JSON.parse(data);
let i = 0;
let pushData = [];
newData.RECORDS.map(function(value,index){
if(value.IMAGE1 != null){
i++;
console.log(value.NAME);
pushData.push(value);
}
})
console.log(i);
console.log(pushData);
})

写入到新的文件中

1
2
3
4
fs.writeFile('./newGoods.json',JSON.stringify(pushData),function(err){
if(err) console.log('写入文件失败!');
else console.log('写入文件操作成功');
})

这样就完成了这次数据的提纯操作,通过提纯我们得到了一张可用的 json 商品详情表。

通过这节的学习主要是了解一下 node 的 fs 模块,这在工作中是非常常用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//	fsJson.js 文件完整代码:

const fs = require('fs');

fs.readFile('.goods.json', 'utf8', function(err, data){
let newData= JSON.parse(data)
let i=0;
let pushData=[];
newData.RECORDS.map(function(value,index){
if(value.IMAGE1!=null){
i++;
console.log(value.NAME);
pushData.push(value);
}
});
console.log(i);
console.log(pushData);
fs.writeFile('./newGoods.json',JSON.stringify(pushData),function(err){
if(err) console.log('写文件操作失败');
else console.log('写文件操作成功');
});
});

批量插入商品详情数据到 MongoDB 中

有了可插入的 JSON 数据,就可以建立一个 Schema 模型, 然后是用 Mongoose 插入到我们的数据库中。

1. 建立 Goods 的 Schema

建立 service/database/schema/Goods.js 文件,然后根据我们的数据表结构建立模型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const mongoose = require('mongoose')		//	引入 Mongoose
const Schema = mongoose.Schema // 声明 Schema
let ObjectId = Schema.Types.ObjectId // 声明 Object 类型

const goodsSchema = new Schema({
ID:{unique:true,type:String},
GOODS_SERIAL_NUMBER:String,
SHOP_ID:String,
SUB_ID:String,
GOOD_TYPE:Number,
STATE:Number,
NAME:String,
ORI_PRICE:Number,
PRESENT_PRICE:Number,
AMOUNT:Number,
DETAIL:String,
BRIEF:String,
SALES_COUNT:Number,
IMAGE1:String,
IMAGE2:String,
IMAGE3:String,
IMAGE4:String,
IMAGE5:String,
ORIGIN_PLACE:String,
GOOD_SCENT:String,
CREATE_TIME:String,
UPDATE_TIME:String,
IS_RECOMMEND:Number,
PICTURE_COMPERSS_PATH:String
},{
collections:'Goods'
})

mongoose.model('Goods',goodsSchema)

// 这里需要注意的是,要安装我们 JSON 中的数据进行建立。

2. 批量插入数据库的路由方法

新建一个 service/appApi/goods.js 以后关于商品的操作就都在这个 api 文件中编写了,我们也是要写路由的形式,提供每一个支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
let router = new Router();

const mongoose = require('mongoose');
const fs = require('fs');

router.get('/insertAllGoodsInfo',async(ctx)=>{
fs.readFile('./goods.json','utf8',(err,data)=>{
data = JSON.parse(data);
let saveCount = 0;
const Goods = mongoose.model('Goods');
data.map((value,index)=>{
console.log(value);
let newGoods = new Goods(value);
newGoods.save().then(()=>{
saveCount++;
console.log('成功' + saveCount)
}).catch(error => {
console.log('失败:' + error)
})
})
})
ctx.body = "开始导入数据!"
})

module.exports = router;

3. 把路由加入到 index.js 里

写好了 goods.js 的路由方法,就可以把它暴露到 index.js 里了

1
2
3
4
//	/service/index.js

let goods = require('./appApi/goods.js');
router.use('/goods',goods.routes())

做完这一步,就可以在浏览器运行一下 http://localhost:3000/goods/insertAllGoodsInfo,运行结束后,可以到数据库看一下插入情况批量插入商品详情数据到 MongoDB 中

有了可插入的 JSON 数据,就可以建立一个 Schema 模型, 然后是用 Mongoose 插入到我们的数据库中。

1. 建立 Goods 的 Schema

建立 service/database/schema/Goods.js 文件,然后根据我们的数据表结构建立模型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const mongoose = require('mongoose')		//	引入 Mongoose
const Schema = mongoose.Schema // 声明 Schema
let ObjectId = Schema.Types.ObjectId // 声明 Object 类型

const goodsSchema = new Schema({
ID:{unique:true,type:String},
GOODS_SERIAL_NUMBER:String,
SHOP_ID:String,
SUB_ID:String,
GOOD_TYPE:Number,
STATE:Number,
NAME:String,
ORI_PRICE:Number,
PRESENT_PRICE:Number,
AMOUNT:Number,
DETAIL:String,
BRIEF:String,
SALES_COUNT:Number,
IMAGE1:String,
IMAGE2:String,
IMAGE3:String,
IMAGE4:String,
IMAGE5:String,
ORIGIN_PLACE:String,
GOOD_SCENT:String,
CREATE_TIME:String,
UPDATE_TIME:String,
IS_RECOMMEND:Number,
PICTURE_COMPERSS_PATH:String
},{
collections:'Goods'
})

mongoose.model('Goods',goodsSchema)

// 这里需要注意的是,要安装我们 JSON 中的数据进行建立。

2. 批量插入数据库的路由方法

新建一个 service/appApi/goods.js 以后关于商品的操作就都在这个 api 文件中编写了,我们也是要写路由的形式,提供每一个支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
let router = new Router();

const mongoose = require('mongoose');
const fs = require('fs');

router.get('/insertAllGoodsInfo',async(ctx)=>{
fs.readFile('./goods.json','utf8',(err,data)=>{
data = JSON.parse(data);
let saveCount = 0;
const Goods = mongoose.model('Goods');
data.map((value,index)=>{
console.log(value);
let newGoods = new Goods(value);
newGoods.save().then(()=>{
saveCount++;
console.log('成功' + saveCount)
}).catch(error => {
console.log('失败:' + error)
})
})
})
ctx.body = "开始导入数据!"
})

module.exports = router;

3. 把路由加入到 index.js 里

写好了 goods.js 的路由方法,就可以把它暴露到 index.js 里了

1
2
3
4
//	/service/index.js

let goods = require('./appApi/goods.js');
router.use('/goods',goods.routes())

做完这一步,就可以在浏览器运行一下 http://localhost:3000/goods/insertAllGoodsInfo,运行结束后,可以到数据库看一下插入情况批量插入商品详情数据到 MongoDB 中

有了可插入的 JSON 数据,就可以建立一个 Schema 模型, 然后是用 Mongoose 插入到我们的数据库中。

1. 建立 Goods 的 Schema

建立 service/database/schema/Goods.js 文件,然后根据我们的数据表结构建立模型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const mongoose = require('mongoose')		//	引入 Mongoose
const Schema = mongoose.Schema // 声明 Schema
let ObjectId = Schema.Types.ObjectId // 声明 Object 类型

const goodsSchema = new Schema({
ID:{unique:true,type:String},
GOODS_SERIAL_NUMBER:String,
SHOP_ID:String,
SUB_ID:String,
GOOD_TYPE:Number,
STATE:Number,
NAME:String,
ORI_PRICE:Number,
PRESENT_PRICE:Number,
AMOUNT:Number,
DETAIL:String,
BRIEF:String,
SALES_COUNT:Number,
IMAGE1:String,
IMAGE2:String,
IMAGE3:String,
IMAGE4:String,
IMAGE5:String,
ORIGIN_PLACE:String,
GOOD_SCENT:String,
CREATE_TIME:String,
UPDATE_TIME:String,
IS_RECOMMEND:Number,
PICTURE_COMPERSS_PATH:String
},{
collections:'Goods'
})

mongoose.model('Goods',goodsSchema)

// 这里需要注意的是,要安装我们 JSON 中的数据进行建立。

2. 批量插入数据库的路由方法

新建一个 service/appApi/goods.js 以后关于商品的操作就都在这个 api 文件中编写了,我们也是要写路由的形式,提供每一个支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');
let router = new Router();

const mongoose = require('mongoose');
const fs = require('fs');

router.get('/insertAllGoodsInfo',async(ctx)=>{
fs.readFile('./goods.json','utf8',(err,data)=>{
data = JSON.parse(data);
let saveCount = 0;
const Goods = mongoose.model('Goods');
data.map((value,index)=>{
console.log(value);
let newGoods = new Goods(value);
newGoods.save().then(()=>{
saveCount++;
console.log('成功' + saveCount)
}).catch(error => {
console.log('失败:' + error)
})
})
})
ctx.body = "开始导入数据!"
})

module.exports = router;

3. 把路由加入到 index.js 里

写好了 goods.js 的路由方法,就可以把它暴露到 index.js 里了

1
2
3
4
//	/service/index.js

let goods = require('./appApi/goods.js');
router.use('/goods',goods.routes())

做完这一步,就可以在浏览器运行一下 http://localhost:3000/goods/insertAllGoodsInfo,运行结束后,可以到数据库看一下插入情况

商品大类的 Shema 建立和导入数据库

这节主要是把商品大类的数据从 JSON 格式导入到 MongoDB 数据库中,涉及到的知识点有 Schema 的建立, fs 模块的使用和 Mongoose 的存储。

1. 编写 Category 的 Schema

可以把 Schema 想象成数据库 collections 的一个复制映射,它是和数据库里的 collection 对应的。明白了这个,我们直接根据 JSON 的格式直接制作就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//	/service/database/schema/Category.js

const mongoose = require('mongoose') //引入Mongoose
const Schema = mongoose.Schema //声明Schema

const categorySchema = new Schema({
ID:{unique:true,type:String},
MALL_CATEGORY_NAME:{type:String},
IMAGE:{type:String},
TYPE:{type:Number},
SORT:{type:Number},
COMMENTS:{type:String}
})

mongoose.model('Category',categorySchema)

插入 MongoDB 数据库

有了 Schema 之后可以在 service/appApi/goods.js 文件里增加一个路由配置,并把业务逻辑代码写入到路由里。

  • 用 fs 读取 category.json 的数据
  • 把数据进行循环存入数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
router.get('/insertAllCategory',async(ctx)=>{
fs.readFile('./data_json/category.json','utf8',(err,data)=>{
data=JSON.parse(data)
let saveCount=0
const Category = mongoose.model('Category')
data.RECORDS.map((value,index)=>{
console.log(value)
let newCategory = new Category(value)
newCategory.save().then(()=>{
saveCount++
console.log('成功'+saveCount)
}).catch(error=>{
console.log('失败:'+error)
})
})
})
ctx.body="开始导入数据"
})

完成后可以打开 mongod 服务, 然后访问 http://localhost:3000/goods/insertAllCategory,数据就可以顺利插入到数据库离了。子数据的操作也是差不多一样的。

商品子类的 Shema 建立和导入数据库

1. categorySub 的 Schema 建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//	service/database/schema/

const mongoose = require('mongoose') // 引入 Mongoose
const Schema = mongoose.Schema // 声明 Schema

const categorySubSchema = new Schema({
ID:{unique:true,type:String},
MALL_CATEGORY_ID:{type:String},
MALL_SUB_NAME:{type:String},
COMMENTS:{type:String},
SORT:{type:Number}
})

mongoose.model('CategorySub',categorySubSchema)

2. 保存到数据库的业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//	service/appApi/goods.js

router.get('/insertAllCategorySub',async(ctx)=>{
fs.readFile('./data_json/category_sub.json','utf8',(err,data)=>{
data = JSON.parse(data);
let saveCount = 0;
const CategorySub = mongoose.model('CategorySub');
data.RECORDS.map((value,index)=>{
console.log(value);
let newCategorySub = new CategorySub(value)
newCategorySub.save().then(()=>{
saveCount++;
console.log('成功插入'+saveCount)
}).catch(error=>{
console.log('插入失败'+error)
})
})
})
ctx.body = "开始导入数据"
})

编写商品详情页的数据接口

1. 编写后台数据接口

在 service/appApi/goods.js 里,新编写一个路由业务逻辑,并用 findOne 的形式查找出一条商品数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*	获取商品详细信息的接口  */
router.post('/getDetailGoodsInfo',async(ctx)=>{
let goodsId = ctx.request.body.goodsId;
const Goods = mongoose.model('Goods');
await Goods.findOne({ID:goodsId}).exec()
.then(async(result) => {
ctx.body = {code:200,message:result}
})
.catch(error => {
console.log(error);
ctx.body = {code:500,message:error}
})
})

首先获得了从前端得到的参数 goodsId, 然后得到 Goods 模型, 用模型的 findOne 方法查找数据,查找出来进行返回。

2. 编写 Goods.vue 页面

由于现在还不能正常查看接口,所以我们需要一个页面,来调用接口。 新建 /src/components/pages/Goods.vue 文件。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div>
商品详情页面
</div>
</template>

<script>
import axios from 'axios'
import url from '@/serviceAPI.config.js'
export default {
data() {
return {
goodsId: '775e575ce28a4f89b1dfe2c99eb08ae7'
}
},
created(){

this.getInfo()
},
methods: {
getInfo() {
axios({
url:url.getDetailGoodsInfo,
method:'post',
data:{
goodsId:this.goodsId
}
})
.then(response=>{
console.log(response)
})
.catch(error=>{
console.log(error)
})
}
},
}
</script>

<style scoped>

</style>

3. 把组件加入到路由管理器中

有了模块之后,我们需要把模块加入到路由管理之中,这样才可以正常访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Router from 'vue-router'
import ShoppingMall from '@/components/pages/ShoppingMall'
import Login from '@/components/pages/Login'
import Register from '@/components/pages/Register'
import Goods from '@/components/pages/Goods'

Vue.use(Router)

export default new Router({
routes: [
{path: '/',name: 'ShoppingMall',component: ShoppingMall},
{path: '/register',name: 'Register',component: Register},
{path: '/login',name: 'Login',component: Login},
{path: '/Goods',name: 'Goods',component: Goods},
]
})

现在可以直接打开 http://localhost:8080/#/Goods 路径,然后打开控制台,可以发现已经顺利的从后台取得数据了。

改写程序,让程序更优雅

1
2
3
4
5
6
7
8
9
10
11
//	把上面的代码进行改写,改成只用 async/await 的方式
router.post('/getDetailGoodsInfo',async(ctx) => {
try{
let goodsId = ctx.request.body.goodsId;
const Goods = mongoose.model('Goods');
let result = await Goods.findOne({ID: goodsId}).exec();
ctx.body = {code:200,message:result}
} catch(err) {
ctx.body = {code:500,message:err}
}
})

商品详情页路由的制作和参数的传递

修改 goodsInfoComponent.vue 文件

修改这个文件主要是让它具有跳转能力和传递参数的能力:

  1. 在这个组件里新加入一个 props,接受 goodsId
  2. 编写一个页面跳转的方法,这里起名为 goGoodsPage
  3. 绑定单击事件进行跳转 @click=’goGoodsPage’

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div class="goods-info" @click="goGoodsPage()">
<div class="goods-image">
<img v-lazy="goodsImage" width="90%" />
</div>
<div class="goods-name">{{goodsName}}</div>
<div class="goods-price">¥{{goodsPrice | moneyFilter }}</div>
</div>
</template>

<script>
import {toMoney} from '@/filter/moneyFilter.js'
export default {
props:['goodsImage','goodsName','goodsPrice','goodsId'],
filters:{
moneyFilter(money){
return toMoney(money)
}
},
methods: {
goGoodsPage() {
this.$router.push({name:'Goods',query:{goodsId:this.goodsId}})
}
},
}
</script>

<style scoped>
.goods-name{
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space:nowrap;
}
</style>

接收路由传递的参数

接收参数可以使用 this.$route.query.goodsId,这样就可以得到由其他页面传递过来的参数了。

在 src/components/pages/Goods.vue 的 created 生命周期里修改如下:

1
2
3
4
5
created(){
this.goodsId = this.$route.query.goodsId;
console.log(this.goodsId);
this.getInfo();
},

改写首页,传递 goodsId 参数

在 src/components/pages/ShoppingMall.vue 里,热卖商品的属性部分绑定 goodsId- :goodsID=’item.goodsId’ 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Hot Area-->
<div class="hot-area">
<div class="hot-title">热卖商品</div>
<div class="hot-goods">
<van-list>
<van-row gutter="20">
<van-col span="12" v-for="(item,index) in hotGoods" :key="index">
<goods-info :goodsId="item.goodsId" :goodsImage="item.image" :goodsName="item.name" :goodsPrice="item.price"></goods-info>
</van-col>
</van-row>
</van-list>
</div>
</div>

商品详情的页面模板编写1

1. 引入 Vant 框架中的 NavBar 组件

在项目的 /src/main.js 文件中用 import 引入 NavBar 组件

import { NavBar } from ‘vant’

Vue.use(NavNar)

2. 使用 NavBar 制作头部导航

打开 /src/components/pages/Goods.vue 文件,编写 template 部分代码,直接使用 van-nav-bar 组件

1
2
3
4
5
6
7
8
9
10
<div>
<div class="navbar-div">
<van-nav-bar
title="商品详情"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
</div>
</div>
  • title: 是显示的标题,这里就起名字叫做商品详情
  • left-text: 是左侧显示的内容,治理显示 “返回” 两个字
  • left-arrow:是否显示左侧箭头,默认值是 true,也就是显示
  • @click-left:绑定左侧按钮时触发的事件方法,方法名字叫做 onClickLeft

编写 onClickLeft 方法,这个方法里边现在只要能回退到上一层路由就可以了,暂时不写其他逻辑,以后使用了页面缓存功能后,还要增加消除缓存的操作。

1
2
3
onClickLeft(){
this.$router.go(-1);
},

打开浏览器,测试一下是否点击返回按钮可以实现返回上一级路由

3. 编写商品图片部分

直接使用 html 中的 img 标签把头图显示出来

1
2
3
<div class="topimage-div">
<img :src="goodsInfo.IMAGE1" width="100%">
</div>

这里还没有 goodsInfo 注册数据,所以还需要在 js 部分的 data 中先进行注册

1
2
3
4
5
6
data() {
return {
goodsId: '',
goodsInfo: {}, // 商品详细数据
}
}

然后在 getInfo 方法里, 对 goodsInfo 进行赋值:this.goodsInfo = response.data.message

直接这样写不够严谨,应该在赋值前判断一下 code 的值和 message 不为空,代码如下:

1
2
3
4
5
6
if(response.data.code == 200 && response.data.message ){
this.goodsInfo = response.data.message
}else{
Toast('服务器错误,数据取得失败')
}
console.log( this.goodsInfo )

商品详情的页面模板编写2

修改商品详情页面的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<template>
<div>
<div class="navbar-div">
<van-nav-bar
title="商品详情"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
</div>
<div class="topimage-div">
<img :src="goodsInfo.IMAGE1" width="100%"/>
</div>
<div class="goods-name">{{goodsInfo.NAME}}</div>
<div class="goods-price">价格:{{goodsInfo.PRESENT_PRICE}}</div>
<div>
<van-tabs >
<van-tab title="商品详情">
<div class="detail" v-html="goodsInfo.DETAIL">

</div>
</van-tab>
<van-tab title="评价">
正在制作中
</van-tab>
</van-tabs>

</div>
</div>
</template>

<script>
import axios from 'axios'
import url from '@/serviceAPI.config.js'
import { Toast } from 'vant'
export default {
data() {
return {
goodsId: '',
goodsInfo:{}, //商品详细数据
}
},
created(){
this.goodsId= this.$route.query.goodsId
console.log(this.goodsId)
this.getInfo()
},
methods: {
onClickLeft(){
this.$router.go(-1)
},

getInfo() {
axios({
url:url.getDetailGoodsInfo,
method:'post',
data:{
goodsId:this.goodsId
}
})
.then(response=>{
if(response.data.code == 200 && response.data.message ){
this.goodsInfo = response.data.message
}else{
Toast('服务器错误,数据取得失败')
}
console.log( this.goodsInfo)
})
.catch(error=>{
console.log(error)
})
}
},
}
</script>

<style scoped>
.detail{
font-size:0px;
}
.goods-name{
background-color: #fff;
}
.goods-price{
background-color: #fff;
}
</style>

vant 标签组件的使用

  1. 首先引入标签组件,在 main.js 里直接引入。 代码如下:

    1
    2
    3
    4
    5
    6
    import { Tab,Tabs } from 'vant'
    Vue.use(Field)
    .use(Tab)
    .use(Tabs)

    // 引入之后就可以在商品组件中直接使用了
  2. 编写 template 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <van-tabs >
    <van-tab title="商品详情">
    <div class="detail" v-html="goodsInfo.DETAIL">
    </div>
    </van-tab>
    <van-tab title="评价">
    正在制作中
    </van-tab>
    </van-tabs>

解决图片有空隙问题

因为每个图片后面是有空格的,而图片占了宽度的 100%,所以空格被单独挤出了一行。

解决方法: 把字体设置为 0,但是这样做如果以后有图文混排就会出现不显示字体的 BUG。所以最好的就决方案是后端插入的时候就取消掉空格。

1
2
3
.detail {
font-size: 0px;
}

商品详情的页面模板编写3

加入价格的过滤器和底部的加入购物车和购买按钮

加入 Filter 过滤器来格式化价格

页面中的价格并没有进行格式化,需要一个 Filter 来进行格式化,在制作首页的时候,已经制作了一个 Filter,直接使用就可以进行格式化了。

  1. 引入 moneyFilter.js 文件

    1
    import { toMoney } from '@/filter/moneyFilter.js'
  2. 在 js 部分编写 filters 属性

    1
    2
    3
    4
    5
    filters: {
    moneyFilter( money ){
    return toMoney( money )
    }
    },
  3. 给价格加上 Filter

    1
    {{ goodsInfo.PRESENT_PRICE | moneyFilter }}

通过上面的三步,就可以实现价格的格式化了。

底部购买按钮和加入购物车按钮

在 template 的最底部,加入一个层,并进行基本的 flex 布局

1
2
3
4
5
6
7
8
<div class="goods-bottom">
<div>
<van-button size="large" type="primary">加入购物车</van-button>
</div>
<div>
<van-button size="large" type="danger">直接购买</van-button>
</div>
</div>

编写 CSS 样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.goods-bottom{
position: fixed;
bottom:0px;
left:0px;
background-color: #FFF;
width:100%;

display: flex;
flex-direction: row;
flex-flow: nowrap;
}
.goods-bottom &gt; div{
flex:1;
padding:5px;
}

分类页面的数据读取

读取大类别的 API 制作

首先获取大类信息,在 /service/appApi/goods.js 里增加一个新的路由 getCategoryList 代码如下:

1
2
3
4
5
6
7
8
9
router.get('/getCategoryList',async(ctx) => {
try{
const Category = mongoose.model('Category');
let result = await Category.find().exec();
ctx.body = {code:200, message:result};
} catch (err) {
ctx.body = {code: 500, message: err}
}
})

打开浏览器进行测试 http://localhost:3000/goods/getCategoryList

读取小类别的 API 制作

在 /service/appApi/goods.js 里增加一个新的路由 getCategorySubList 代码如下:

1
2
3
4
5
6
7
8
9
10
11
router.get('/getCategorySubList',async(ctx) => {
try{
// let categoryId = ctx.request.body.categoryId
let categoryId = 1;
const CategorySub = mongoose.model('CategorySub');
let result = await CategorySub.find({MALL_CATEGORY_ID:categoryId}).exec();
ctx.body = {code:200, message:result}
} catch(err) {
ctx.body = {code:500, message:err}
}
})

打开浏览器进行测试, http://localhost:3000/goods/getCategorySubList

根据商品类别获取商品列表

在 /service/appApi/goods.js 里新增加一个新的路由 getGoodsListByCategorySubID 代码如下:

1
2
3
4
5
6
7
8
9
10
11
router.get('/getGoodsListByCategorySubID',async(ctx) => {
try{
// let categorySubId = ctx.request.body.categoryId
let categorySubId = '2c9f6c946016ea9b016016f79c8e0000';
const Goods = mongoose.model('Goods');
let result = await Goods.find({SUB_ID:categorySubId}).exec();
ctx.body = {code:200, message: result};
} catch(err) {
ctx.body = {code:500, message:err}
}
})

打开浏览器进行测试 http://localhost:3000/goods/getGoodsListByCategorySubID

商品详细页的滑动切换和吸顶效果

Tab 页的滑动切换

通过 van-tabs 里的 swipeable 属性就可以开启滑动切换 tab 页的效果,代码如下:

1
2
3
4
5
6
7
8
<van-tabs  swipeable>
<van-tab title="商品详情">
<div class="detail" v-html="goodsInfo.DETAIL"></div>
</van-tab>
<van-tab title="评价">
正在制作中
</van-tab>
</van-tabs>

吸顶效果的制作

通过 van-tabs 里的 sticky 属性可以开启吸顶效果,也叫粘性布局,当 Tab 滚动到顶部时会自动吸顶。

1
2
3
4
5
6
7
8
<van-tabs  swipeable sticky>
<van-tab title="商品详情">
<div class="detail" v-html="goodsInfo.DETAIL"></div>
</van-tab>
<van-tab title="评价">
正在制作中
</van-tab>
</van-tabs>

商品列表页的布局1

建立页面和配置列表页路由

在 /src/components/pages 文件夹下新建一个 CategoryList.vue 页面。 然后利用 vbase 命令快速建立基本结构。

有了基本的页面后,到 /src/router/index.js 文件里, 添加 CategoryList.vue 页面的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from 'vue'
import Router from 'vue-router'
import ShoppingMall from '@/components/pages/ShoppingMall'
import Login from '@/components/pages/Login'
import Register from '@/components/pages/Register'
import Goods from '@/components/pages/Goods'
import CategoryList from '@/components/pages/CategoryList'

Vue.use(Router)

export default new Router({
routes: [
{path: '/',name: 'ShoppingMall',component: ShoppingMall},
{path: '/register',name: 'Register',component: Register},
{path: '/login',name: 'Login',component: Login},
{path: '/Goods',name: 'Goods',component: Goods},
{path: '/CategoryList',name: 'CategoryList',component: CategoryList},
]
})

做完这步,我们的页面就可以正常访问到了,访问地址 http://localhost:8080/#/CategoryList

标题栏的布局

使用 Vant 提供好的 van-nav-bar 组件为页面添加一个标题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<div class="navbar-div">
<van-nav-bar title="类别列表" />
</div>
</div>
</template>

<script>
export default {

}
</script>

<style scoped>

</style>

如果希望它固定在头部, 可以加入 fixed 属性。

大类的侧边栏的布局

使用 row-col 进行布局,代码如下:

1
2
3
4
5
6
7
8
9
10
<div>
<van-row>
<van-col span="6">
<div id="leftNav">
左侧导航
</div>
</van-col>
<van-col span="18">右侧列表</van-col>
</van-row>
</div>

axios 读取左侧大类

  1. 用 import 先导入 axios 和 serviceAPI.config.js,代码如下:

    import axios from ‘axios’

    import url from ‘@/serviceAPI.config.js’

  2. 配置 serviceAPI.config.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const BASEURL = "https://www.easy-mock.com/mock/5ae2eeb23fbbf24d8cd7f0b6/SmileVue/"
    const LOCALURL = "http://localhost:3000/"
    const URL = {
    getShoppingMallInfo:BASEURL+'index',
    getGoodsInfo:BASEURL+'getGoodsInfo',
    registerUser:LOCALURL+'user/register', //用户注册接口
    login:LOCALURL+'user/login', //用户注册接口
    getDetailGoodsInfo:LOCALURL+'goods/getDetailGoodsInfo', //得到商品详细数据
    getCategoryList:LOCALURL+'goods/getCategoryList', //得到大类信息

    }

    module.exports = URL
  3. 编写 axios 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    getCategory() {
    axios({
    url:url.getCategoryList,
    method:'get',
    })
    .then(response=>{

    console.log(response)

    if(response.data.code == 200 && response.data.message ){

    }else{
    Toast('服务器错误,数据取得失败')
    }

    })
    .catch(error=>{
    console.log(error)
    })
    }
  4. 在生命周期里加入 getCategory 方法

    1
    2
    3
    created(){
    this.getCategory();
    },

商品列表页的大类交互效果

把大类列表放到左侧导航上

  1. 在 data 属性里注册 category 变量为数组类型

    1
    2
    3
    4
    5
    data() {
    return {
    category: [],
    }
    },
  2. 在 getCategory() 方法里的 axios 的回调方法里为 category 赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    getCategory() {
    axios({
    url: url.getCategoryList,
    method: 'get',
    })
    .then(response => {
    console.log(response)
    if (response.data.code == 200 && response.data.message ){
    this.category = response.data.message
    } else {
    Toast('服务器错误,数据获取失败')
    }
    })
    .catch(error => {
    console.log(error);
    })
    }
  3. 在 template 部分利用 li 标签把数据循环出来

    1
    2
    3
    4
    5
    6
    7
    <div id="leftNav">
    <ul>
    <li v-for="(item,index) in category" :key="index">
    {{item.MALL_CATEGORY_NAME}}
    </li>
    </ul>
    </div>
  4. 编写 CSS 样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #leftNav{
    background-color: aliceblue;
    }
    #leftNav ul li {
    line-height: 2rem;
    border-bottom:1px solid #E4E7ED;
    padding:3px;
    font-size:0.8rem;
    text-align: center;
    }
  5. 在生命周期里加入 js,让左侧适应页面高度

    1
    2
    3
    4
    mounted(){
    let winHeight = document.documentElement.clientHeight;
    document.getElementById("leftNav").style.height = winHeight - 46 + "px";
    },

点击后的交互效果制作 — 反白操作

当点击每个大类的时候,我们希望 CSS 是有所变化的,证明我们已经点击过了,前端也叫做反白操作。

  1. 编写一个背景为白色的 CSS 样式

    1
    2
    3
    .categoryActive{
    background-color: #fff;
    }
  2. 在 data 里注册一个 categoryIndex 变量,用来控制那个导航变成白色,这里默认是 0,就是当打开页面时第一个类别是白色的

    1
    2
    3
    4
    5
    6
    data() {
    return {
    category:[],
    categoryIndex:0,
    }
    },
  3. 编写 clickCategory 方法,点击大类时调用这个方法,方法就是把点击的索引传递过去,然后付给刚才注册的 categoryIndex 属性

    1
    2
    3
    4
    //点击大类的方法
    clickCategory(index){
    this.categoryIndex=index
    }
  4. 在 template 里注册这些方法,并进行 class 动态绑定, 代码如下:

    1
    2
    3
    <li @click="clickCategory(index)" :class="{categoryActive:categoryIndex==index}"  v-for="(item,index) in category" :key="index">
    {{item.MALL_CATEGORY_NAME}}
    </li>

一二级分类的联动效果制作

当点击一级分类的时候,二级分类要根据你点击的一级分类进行变化。可以直接使用 Vant 的 Tab 组件来实现。

在 serviceAPI.config.js 中加入接口

直接把 service 中写的 API 方法,加入到前端接口配置文件中,

getCategorySubList:LOCALURL+'goods/getCategorySubList',,全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const BASEURL = "https://www.easy-mock.com/mock/5ae2eeb23fbbf24d8cd7f0b6/SmileVue/"
const LOCALURL = "http://localhost:3000/"
const URL = {
getShoppingMallInfo:BASEURL+'index',
getGoodsInfo:BASEURL+'getGoodsInfo',
registerUser:LOCALURL+'user/register', //用户注册接口
login:LOCALURL+'user/login', //用户注册接口
getDetailGoodsInfo:LOCALURL+'goods/getDetailGoodsInfo', //得到商品详细数据
getCategoryList:LOCALURL+'goods/getCategoryList', //得到大类信息
getCategorySubList:LOCALURL+'goods/getCategorySubList', //得到小类信息

}

module.exports = URL

改写后端接口

以前写后端接口时,为了测试,所以使用的是 get 方法,但是为了传送数据的安全和方便,现在改为 post 方法,并接受前端传递过来的 categoryId 参数。 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
//	service/appApi/goods.js

router.post('/getCategorySubList',async(ctx)=>{
try{
let categoryId = ctx.request.body.categoryId
const CategorySub = mongoose.model('CategorySub')
let result = await CategorySub.find({MALL_CATEGORY_ID:categoryId}).exec()
ctx.body={code:200,message:result}
}catch(err){
ctx.body={code:500,message:err}
}
})

获取小类的方法

在 CategoryList 文件的 methods 属性里, 加入一个 getCategorySubByCategoryId 方法。 这里主要使用 axios 来获取后端的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//根据大类ID读取小类类别列表
getCategorySubByCategoryId(categoryId){

axios({
url:url.getCategorySubList,
method:'post',
data:{categoryId:categoryId}
})
.then(response=>{
if(response.data.code == 200 && response.data.message ){
this.categorySub=response.data.message
this.active = 0
}else{
Toast('服务器错误,数据取得失败')
}
})
.catch(error=>{
console.log(error)
})
}

改写 clickCategory 方法,加入一个 categoryId 参数,然后在方法里调用刚才写的 getCategorySubByCategoryId 方法,这样就可以实现二级联动效果了。

1
2
3
4
5
6
//点击大类的方法
clickCategory(index,categoryId){
this.categoryIndex=index
this.getCategorySubByCategoryId(categoryId)

},

用 Vant 的 Tabs 组件实现联动

这里直接使用 Vant 提供的 Tabs 组件来实现联动, 代码如下:

1
2
3
4
5
6
7
<div class="tabCategorySub">
<van-tabs v-model="active">
<van-tab v-for="(item, index) in categorySub" :key="index" :title="item.MALL_SUB_NAME">

</van-tab>
</van-tabs>
</div>

现在可以在浏览器中看一下效果了,但是还是有些小 Bug,就是在进入页面的时候是没有二级分类的。这个只要在 getCategory 方法的回调函数里调用一下 getCategorySubByCategoryId 方法就可以了

this.getCategorySubByCategoryId( this.category[0].ID )

这里的 this.category[0].ID 是通过数组索引取得默认的第一个分类的 ID 值

商品列表上拉加载效果的实现

在列表页中都是需要两个基本的功能需求: 上拉加载和下拉刷新。可以使用 Vant 的 List 组件来实现上拉加载效果。

实现上拉加载效果

  1. 首先引入,在 src/main.js 中加入引入代码:

    import { List } from ‘vant’

    Vue.use( List )

  2. 在 data 里声明两个属性: loading 和 finished

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    data(){
    return {
    category: [],
    categoryIndex: 0,
    active: 0,
    categorySub: [],
    list: [],
    loading: false, // 上拉加载使用
    finished: false, // 下拉加载是否没有数据了
    }
    },
  3. 在 methods 属性中编写 onLoad() 方法,用于实现上拉加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    onLoad(){
    setTimeout(()=>{
    for(let i = 0; i < 10; i++) {
    this.list.push(this.list.length+1);
    }
    this.loading = false;
    if ( this.list.length >= 40 ) {
    this.finished = true;
    }
    },500);
    }
  4. 在模板中加入组件代码, 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <div id="list-div">
    <van-list
    v-model="loading"
    :finished="finished"
    @load="onLoad"
    >
    <div class="list-item" v-for="item in list" :key="item">
    {{item}}
    </div>
    </van-list>
    </div>
  5. 预览效果,根据效果编写 CSS 样式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .list-item{
    text-align: center;
    line-height: 80px;
    border-bottom: 1px solid #f0f0f0;
    background-color: #fff;
    }
    #list-div{
    overflow: scroll;
    }
  6. 在 mounted() 生命周期里设置 list-dev 的高度

    1
    2
    3
    4
    5
    mounted(){
    let winHeight = document.documentElement.clientHeight;
    document.getElementById('leftNav').style.height = winHeight - 46 + 'px';
    document.getElementById('list-div').style.height = winHeight - 90 + 'px';
    },

商品列表页下拉刷新效果的实现

列表页的下拉刷新, 并不是指刷新整个页面。

  1. 引入 Vant 中的 PullRefresh 组件

    引入 pullRefresh 组件就可以实现下拉刷新效果,先在 src/main.js 中引入一下。

    import { PullRefresh } from ‘vant’

    Vue.use(PullRefresh)

  2. 增加下拉刷新用的变量和方法

    在 data 中增加一个 isRefresh 属性,用来说明现在的状态是否是下拉加载状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    data(){
    return {
    category: [],
    categoryIndex: 0,
    active: 0,
    categorySub: [],
    list: [],
    loading: false, // 上拉加载使用
    finished: false, // 上拉加载是否没有了
    isRefresh: false, // 下拉加载
    }
    }

    然后写一个 onRefresh 方法,用来重新加载数据

    1
    2
    3
    4
    5
    6
    7
    onRefresh(){
    setTimeout( ()=>{
    this.isRefresh = false;
    this.list = [];
    this.onLoad();
    }, 500);
    }

    先把 list 数组清空,然后再次调用 onLoad() 方法,这样就会刷新商品列表页面了

  3. 编写 template 部分

    直接在 van-list 组件外边加入 van-pull-refresh 组件就可以实现了,具体代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <div id="list-div">
    <van-pull-refresh v-model="isRefresh" @refresh="onRefresh">
    <van-list
    v-model="loading"
    :finished="finished"
    @load="onLoad"
    >
    <div class="list-item" v-for="item in list" :key="item">
    {{item}}
    </div>
    </van-list>
    </van-pull-refresh>
    </div>

商品类别分类的 Koa2 分页服务制作

修改 Koa2 服务代码

进入 service/appApi/goods.js 找到 getGoodsListByCategorySubID 这个请求进行改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.post('/getGoodsListByCategorySubID',async(ctx)=>{
try{
let categorySubId = ctx.request.body.categorySubId //小类别
let page =ctx.request.body.page
let num = 10 //每页显示数量
let start = (page-1)*num
//let categorySubId = '2c9f6c946016ea9b016016f79c8e0000'
const Goods = mongoose.model('Goods')
let result = await Goods.find({SUB_ID:categorySubId})
.skip(start) .limit(num).exec()
ctx.body={code:200,message:result}
}catch(err){
ctx.body={code:500,message:err}
}
})

首先把请求方式改为了 post,然后从前台接收两个数据 categorySubId ( 子类ID号 ) 和 page ( 当前页数 )。定义每页显示的数量,然后再根据每页数量和当前页数算出开始读取的位置 start 。有了开始位置和每页显示数量,就尅顺利得出我们想要的结果了。

把写好的接口加入到前台

打开 src/serviceAPI.config.js 文件,在最后加入代码:

getGoodsListByCategorySubID:LOCALURL+’goods/getGoodsListByCategorySubID’, //得到小类商品信息

此时全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const BASEURL = "https://www.easy-mock.com/mock/5ae2eeb23fbbf24d8cd7f0b6/SmileVue/"
const LOCALURL = "http://localhost:3000/"
const URL = {
getShoppingMallInfo:BASEURL+'index',
getGoodsInfo:BASEURL+'getGoodsInfo',
registerUser:LOCALURL+'user/register', //用户注册接口
login:LOCALURL+'user/login', //用户注册接口
getDetailGoodsInfo:LOCALURL+'goods/getDetailGoodsInfo', //得到商品详细数据
getCategoryList:LOCALURL+'goods/getCategoryList', //得到大类信息
getCategorySubList:LOCALURL+'goods/getCategorySubList', //得到小类信息
getGoodsListByCategorySubID:LOCALURL+'goods/getGoodsListByCategorySubID', //得到小类商品信息

}
module.exports = URL

真实数据的上拉加载效果制作

在 data 属性里注册必要的参数

在 src/components/pages/CategoryList.vue 里注册几个属性,代码如下:

1
2
3
4
5
6
7
data () {
return {
page: 1, // 商品列表的页数
goodList: [], // 商品信息
categorySubId: '' // 商品字分类ID
}
},

编写 axios 商品列表的获取方法

增加 getGoodList 方法,这个方法里传递两个参数,第一个是商品的子分类,第二个是请求分类的页数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
getGoodList(){
axios({
url: url.getGoodListByCategorySubID,
method: 'post',
data: {
categorySubId: this.categorySubId,
page: this.page
}
}).then(response => {
console.log(response);
if(response.data.code == 200 && response.data.message.length){
this.page++;
this.goodList = this.goodList.concat(response.data.message)
} else {
this.finished = true;
}
this.loading = false;
console.log(this.finished);
}).catch(error => {
console.log(error);
})
}

点击获取子类商品信息的方法

编写点击子类 Tab,就可以获取子类 ID 的方法,在获取子类 ID 的同时,需要做一些变量的初始化操作,比如把 goodsList 变量清空。

1
2
3
4
5
6
7
8
9
10
//	点击子类获取商品信息
onClickCategorySub(index,title){
this.categorySubId = this.categorySub[index].ID;
console.log(this.categorySubId);

this.goodList = [];
this.finished = false;
this.page = 1;
this.onLoad();
}

改造大类的方法

在点击大类时,同样也要进行一些初始化操作

1
2
3
4
5
6
7
8
//	点击大类的方法
clickCategory(index,categoryId){
this.categoryIndex = index;
this.page = 1;
this.finished = false;
this.goodList = [];
this.getCategorySubByCategoryId(categoryId);
},

编写 OnLoad 方法,实现上拉加载效果

1
2
3
4
5
6
onLoad(){
setTimeout(()=>{
this.categorySubId = this.categorySubId ? this.categorySubId : this.categorySub[0].ID;
this.getGoodList();
},1000)
},

编写 html 和 CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//	HTML 部分
<van-list
v-model="loading"
:finished="finished"
@load="onLoad"
>
<div class="list-item" v-for="(item,index) in goodList" :key="index">
<div class="list-item-img"><img :src="item.IMAGE1" width="100%"/></div>
<div class="list-item-text">
<div>{{item.NAME}}</div>
<div class="">¥{{item.ORI_PRICE}}</div>
</div>
</div>
</van-list>

// CSS 部分
.list-item{
display: flex;
flex-direction: row;
font-size:0.8rem;
border-bottom: 1px solid #f0f0f0;
background-color: #fff;
padding:5px;
}
#list-div{
overflow: scroll;
}
.list-item-img{
flex:8;
}
.list-item-text{
flex:16;
margin-top:10px;
margin-left:10px;
}

真实数据的下拉刷新效果制作

下拉刷新效果的制作

在 /src/components/pages/CategoryList.vue 文件中改写 onRefresh() 方法

1
2
3
4
5
6
7
8
9
onRefresh(){
setTimeout(() => {
this.isRefresh = false;
this.finished = false;
this.goodList = [];
this.page = 1;
this.onLoad();
},500)
},

解决首页类别导航图片大小不一的 BUG

  1. 先给 导航栏的层 添加一个 type-item 的 class

    1
    2
    3
    4
    5
    6
    7
    //	ShoppingMall.vue
    <div class="type-bar">
    <div class="type-item" v-for="(cate,index) in category" :key="index" >
    <img v-lazy="cate.image" width="90%" />
    <span>{{cate.mallCategoryName}}</span>
    </div>
    </div>
  2. 然后在 CSS 部分加入一句 CSS 代码

    1
    2
    3
    .type-item {
    flex: 1;
    }

    或者不用修改 html 部分,直接只添加一行 CSS 代码也可以

    1
    2
    3
    .type-bar div {
    flex: 1;
    }

Vue 中图片失效替补图片的制作方法

在获取的商品信息中,有一些商品图片已经失效,如果在真实的项目中遇到这类问题,是不允许直接显示图片失效的,所以我们需要显示一个替补图片

制作一张替补图片

使用 PS 制作一张替补图片,尽量不要使用彩色,而是使用黑白灰来制作。或者直接在网上随便找一张。

把图片放到相应位置

把图片命名为 errorimg.png, 并放到 /src/assets/images/errorimg.png.

然后在 src/components/pages/CategoryList.vue 中的 data 中加入 errorImg 属性,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data() {
return {
category:[],
categoryIndex:0,
active:0,
categorySub:[],
list:[],
loading:false, //上拉加载使用
finished:false, //上拉加载是否没有了?
isRefresh:false, //下拉加载
page:1, //商品列表的页数
goodList:[], //商品信息
categorySubId:'', //商品子分类ID
errorImg:'this.src="' + require('@/assets/images/errorimg.png') + '"' , //错误图片显示路径
}
},

在图片位置加入 onerror 事件

1
2
3
<div class="list-item-img">
<img :src="item.IMAGE1" width="100%" :onerror="errorImg"/>
</div>

商品列表页编程式导航的制作

编程式导航

  1. 编写一个 goGoodsInfo() 方法,接收参数为 id,这里使用了 name 的导航方式

    1
    2
    3
    goGoodsInfo (id) {
    this.$router.push({name: 'Goods', params: {goodsId: id}})
    }
  2. 给 html 加入 click 事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div class="list-item" @click="goGoodsInfo(item.ID)" v-for="(item,index) in goodList" :key="index" >
    <div class="list-item-img">
    <img :src="item.IMAGE1" width="100%" :onerror="errorImg"/>
    </div>
    <div class="list-item-text">
    <div>{{item.NAME}}</div>
    <div class="">¥{{item.ORI_PRICE}}</div>
    </div>
    </div>
  3. Goods.vue 页面的参数接收改造

    以前使用的是 query 的方法接收,现在改用 params 的方法传递,所以要用到三元运算符做一下兼容处理

    1
    this.goodsId = this.$route.query.goodsId  ?  this.$route.query.goodsId : this.$route.params.goodsId

    params 传参,路径不能使用 path,只能使用 name,不然取不到传的参数。

    1
    this.$router.push({name: 'Goods', params: {goodsId: id}})

    取数据时用 params 获取

    1
    this.$route.params.goodsId

    query 传参,用的是 path,而不是 name,否则也会出错

    1
    this.$router.push({path: '/Goods', query: {goodsId: id}})

    取数据使用 query

    1
    this.$route.query.goodsId

价格过滤器的添加

  1. 首先引入过滤器

    1
    import {toMoney} from '@/filter/moneyFilter.js'
  2. filters 属性的编写

    1
    2
    3
    4
    5
    filters:{
    moneyFilter(money){
    return toMoney(money)
    }
    },
  3. 在 template 中使用

    1
    <div class="">¥ {{item.ORI_PRICE | moneyFilter}}</div>

购物车页面的建立

购物车页面不和后台交互,并且还要保持用户数据的持久化,主要知识点在 H5 新增的 localStorage 本地存储里

建立购物车页面

在 /src/components/pages 目录下新建一个 Cart.vue 文件,然后建立组件的基本结构,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>

</div>
</template>

<script>
export default {

}
</script>

<style scoped>

</style>

加入头部部分

1
2
3
<div class="navbar-div">
<van-nav-bar title="购物车" />
</div>

配置路由

打开 src/router/index.js 文件,增加 Cart 页面的路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from 'vue'
import Router from 'vue-router'
import ShoppingMall from '@/components/pages/ShoppingMall'
import Login from '@/components/pages/Login'
import Register from '@/components/pages/Register'
import Goods from '@/components/pages/Goods'
import CategoryList from '@/components/pages/CategoryList'
import Cart from '@/components/pages/Cart'

Vue.use(Router)

export default new Router({
routes: [
{path: '/',name: 'ShoppingMall',component: ShoppingMall},
{path: '/register',name: 'Register',component: Register},
{path: '/login',name: 'Login',component: Login},
{path: '/Goods',name: 'Goods',component: Goods},
{path: '/CategoryList',name: 'CategoryList',component: CategoryList},
{path: '/Cart',name: 'Cart',component: Cart},
]
})

配置完路由,可以访问 http://localhost:8080/#/Cart 看看是不是可以正常访问

得到购物车数据方法的编写

进入页面要做的第一件事就是取得 localStorage 里的数据,首先在 data 里注册两个属性 cartInfo (购物车中商品的信息) 和 isEmpty (购物车是否为空的标识,方便页面呈现),然后编写具体的 getCartInfo() 方法。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
data() {
return {
cartInfo: [], //购物车内的商品
isEmpty : false , //购物车是否为空,不为空则显示true,为空显示false
}
},
created(){
this.getCartInfo()
},
methods: {
//得到购物车的商品
getCartInfo() {
//判断localStorage里是否有购物车数据
if(localStorage.cartInfo){
//如果有数据,我们去除并赋值给cartInfo
this.cartInfo = JSON.parse(localStorage.cartInfo)
}
//打印到控制台查看效果
console.log(' this.cartInfo:'+JSON.stringify( this.cartInfo))
this.isEmpty = this.cartInfo.length>0 ? true : false
}
},
}

购物车中商品的添加

购物车商品的添加,其实就是对 localStorage 的操作和数组查找的使用,也就是 array.find() 操作。购物车添加的主要逻辑,并不放在 Cart.vue 页面里,我们放在 Goods.vue 页面里,也就是商品详情页面,这样做的好处是以后好扩展,并且不用传递参数,直接操作 localStorage

向购物车中添加商品

在 /components/pages/Goods.vue 里加入一个 addGoodsToCart 方法,然后编写下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
addGoodsToCart(){
//取出购物车内的商品数据
let cartInfo = localStorage.cartInfo ? JSON.parse(localStorage.cartInfo) :[]
//判断购物车内是否已经有这个商品
//如果没有返回undeifnd,如果有返回第一个查找到的数据
let isHaveGoods=cartInfo.find(cart=>cart.goodsId==this.goodsId)
console.log(isHaveGoods)
if(!isHaveGoods){
//没有商品直接添加到数组中
//重新组成添加到购物车的信息
let newGoodsInfo={
goodsId:this.goodsInfo.ID,
Name:this.goodsInfo.NAME,
price:this.goodsInfo.PRESENT_PRICE,
image:this.goodsInfo.IMAGE1,
count:1
}
cartInfo.push(newGoodsInfo) //添加到购物车
localStorage.cartInfo=JSON.stringify(cartInfo) //操作本地数据
Toast.success('添加成功')

}else{
Toast.success('已有此商品')
}
this.$router.push({name:'Cart'}) //进行跳转
}

完成上面代码编写后,向 “加入购物车” 按钮绑定 addGoodsToCart 事件方法

1
<van-button size="large" type="primary" @click="addGoodsToCart">加入购物车</van-button>

购物车清空和商品布局(Flex)

现在购物车里已经可以添加商品数据了,现在来将这些数据显示出来

清空购物车按钮的制作

由于以前存在了一些不合法的数据,现在需要把 localStorage 里的 cartInfo 数据清空,这时候需要一个清空按钮

/src/components/pages/Cart.vue 里的模板里写一个 button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//	清空购物车
<div class="card-title">
<van-button size="small" type="danger" @click="clearCart" plain>清空购物车</van-button>
</div>

// CSS 样式
.card-title{
height: 2rem;
line-height:2rem;
background-color: #fff;
border-bottom:1px solid #E4E7ED;
padding:5px;
text-align: right;
}

// 在 js 部分加入业务逻辑代码,清空 localStorage 里的 cartInfo 数据
clearCart(){
localStorage.removeItem('cartInfo');
this.cartInfo = [];
}

购物车商品的布局

使用 Flex 进行购物车商品的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//	在 main.js 文件中导入 Stepper 组件
import { Stepper } from 'vant';
Vue.use(Stepper);

// 显示购物车中的商品
<div class="cart-list">
<div class="pang-row" v-for="(item,index) in cartInfo" :key="index">
<div class="pang-img"><img :src="item.image" width="100%" /></div>
<div class="pang-text">
<div class="pang-goods-name">{{item.Name}}</div>

<div class="pang-control">
<van-stepper v-model="item.count" />
</div>
</div>
<div class="pang-goods-price">¥{{item.price}}</div>
</div>
</div>


// CSS样式代码如下
.cart-list{
background-color: #fff;
}
.pang-row{
display: flex;
flex-direction: row;
flex-wrap:nowrap;
padding:0.5rem;
font-size:0.85rem;
border-bottom:1px solid #E4E7ED;


}
.pang-img{
flex:6;
}
.pang-text{
flex:14;
padding-left:10px;
}
.pang-control{
padding-top: 10px;
}
.pang-goods-price{
flex:4;
text-align: right;
}

购物车中的商品价格计算

Vue 的双向数据绑定让计算价格变得相当容易,只要对 data 属性里的 cartInfo 属性进行编辑,就会自动呈现在页面上。

商品价格的格式化

引入过滤器,然后直接使用就可以了

  1. 引入 moneyFilter 过滤器

    1
    import {toMoney} from '@/filter/moneyFilter.js'
  2. 编写过滤器

    1
    2
    3
    4
    5
    filters: {
    moneyFilter(money){
    return toMoney(money);
    }
    },
  3. 使用过滤器

    1
    ¥{{item.price | moneyFilter}}

改造 template,增加单个商品总价计算

直接写出数量和每个商品的总价,这里并不需要再 js 里写代码,直接在模板里使用乘法即可完成需求

1
2
3
4
5
6
7
8
9
10
11
<div class="pang-goods-price">
<div>
¥{{item.price | moneyFilter}}
</div>
<div>
x{{item.count}}
</div>
<div class="allPrice">
¥{{item.price*item.count | moneyFilter}}
</div>
</div>

商品总价的计算

商品总价使用 Vue 的计算属性就可以搞定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//	首先编写模板,让商品总价显示出来
<!--显示总金额-->
<div class="totalMoney">
商品总价:¥ {{totalMoney | moneyFilter}}
</div>

// 然后编写 CSS 样式
.totalMoney{
text-align: right;
background-color: #fff;
border-bottom:1px solid #E4E7ED;
padding: 5px;
}

// 最后就是计算属性的编写
computed:{
totalMoney(){
let allMoney = 0
this.cartInfo.forEach((item,index) => {
allMoney += item.price*item.count
});
localStorage.cartInfo=JSON.stringify(this.cartInfo)
return allMoney
}
},

先声明了一个总价格 allMoney,然后用循环读出 cartInfo 里的数据,并给 allMoney 进行赋值,完成后重新写入 localStorage 里,最后返回 allMoney,就完成了计算属性的编写

底部导航栏和子导航的制作

现在已经有了三个页面,我们切换页面还是需要在地址栏输入,然后才能有所变化。现在我们来使用底部导航来切换页面

引入 tabbar 组件

在 /src/main.js 文件里引入两个 Vant 组件 Tabbar 和 Tabbarltem

1
2
import {Tabbar, TabbarItem} from 'vant'
Vue.use(Tannar).use(TabbarItem)

新建 Main.vue 文件

在 /src/components/pages 下新建一个 Main.vue 文件,然后编写底部导航代码,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<div>
<div class="main-div">
<router-view/>
</div>

<van-tabbar v-model="active" @change="changeTabbar(active)">
<van-tabbar-item icon="shop">首页</van-tabbar-item>
<van-tabbar-item icon="records">列表页</van-tabbar-item>
<van-tabbar-item icon="cart">购物车页</van-tabbar-item>
<van-tabbar-item icon="contact" info="20">会员中心</van-tabbar-item>
</van-tabbar>
</div>
</template>

<script>
export default {
data() {
return {
active: 0
}
},
methods: {
changeTabbar(active) {
console.log(active)
switch(active){
case 0:
this.$router.push({name:'ShoppingMall'})
break;
case 1:
this.$router.push({name:'CategoryList'})
break;
case 2:
this.$router.push({name:'Cart'})
break;
case 3:
break;
}
}
},
}
</script>

<style scoped>

</style>

子导航的制作

修改 /src/router/index.js 文件,把 Main.vue 的导航加入进来,然后把商城首页、商品列表页和购物车页面变成他的子导航

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Vue from 'vue'
import Router from 'vue-router'
import ShoppingMall from '@/components/pages/ShoppingMall'
import Login from '@/components/pages/Login'
import Register from '@/components/pages/Register'
import Goods from '@/components/pages/Goods'
import CategoryList from '@/components/pages/CategoryList'
import Cart from '@/components/pages/Cart'
import Main from '@/components/pages/Main'

Vue.use(Router)

export default new Router({
routes: [
{path: '/main',name: 'Main',component: Main,
children:[
{path: '/',name: 'ShoppingMall',component: ShoppingMall},
{path: '/CategoryList',name: 'CategoryList',component: CategoryList},
{path: '/Cart',name: 'Cart',component: Cart},
]
},

{path: '/register',name: 'Register',component: Register},
{path: '/login',name: 'Login',component: Login},
{path: '/Goods',name: 'Goods',component: Goods},


]
}

底部导航栏优化

现在已经有了底部菜单,但是还是有一些小 Bug 需要处理一下。比如说从商品详情页面进入购物车时底部导航不跟随变化,还有有些页面拉不到底部。

购物车页面底部导航的处理

购物车页面出现底部导航不跟随变化的主要问题是:我们不是点击导航进入的,而是通过编程式导航进入的,这样导航栏就没有互动和触发事件,所以并没有发生变化。解决的思路就是跟踪获取路径,根据路径改变导航栏的变化。

  1. 先写一个方法获得路径,然后判断路径是不是 /Cart,是就把 this.active 变成 2 就可以了。代码如下

    1
    2
    3
    4
    5
    6
    changeIabBarActive(){
    this.nowPath = this.$router.path;
    if(this.nowPath == '/Cart') {
    this.active = 2;
    }
    }
  2. 在进入页面的生命周期里引入这个方法

    1
    2
    3
    created(){
    this.changeTabBarActive()
    }

    这时候这个 Bug 就解决了,如果页面中有多种这样的形式,可以逐一进行判断

商城首页不能来到底部的修改

产生这个问题的主要原因是在热销商品的层里没有加入高度,我们在这里个 .hot-goods 加入一段 CSS 样式就可以解决了

1
2
3
4
5
.hot-goods{
height: 130rem;
overflow: hidden;
background-color: #fff;
}

列表页 Bug 的解决

列表页也有拉不到底的问题,因为列表页我们是动态计算出来的高度,所以我们直接在动态计算的地方减去 50px 就可以了

1
2
3
4
5
mounted(){
let winHeight = document.documentElement.clientHeight;
docuemnt.getElementById('leftNav').style.height = winHeight - 46 - 50 + 'px';
document.getElementById('list-div').style.height = winHeight - 90 - 50 + 'px';
},
打赏功能
-------------本文结束感谢您的阅读-------------
0%